1   /*
2    * Copyright (C) 2009 The Guava Authors
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package com.google.common.collect;
18  
19  import com.google.common.base.Function;
20  import com.google.gwt.user.client.Timer;
21  
22  import java.util.LinkedHashMap;
23  import java.util.Map;
24  import java.util.concurrent.ConcurrentHashMap;
25  import java.util.concurrent.ConcurrentMap;
26  import java.util.concurrent.TimeUnit;
27  
28  /**
29   * MapMaker emulation. Since Javascript is single-threaded and have no references, this reduces to
30   * the creation of expiring and computing maps.
31   *
32   * @author Charles Fry
33   */
34  public final class MapMaker extends GenericMapMaker<Object, Object> {
35  
36    // TODO(fry,user): ConcurrentHashMap never throws a CME when mutating the map during iteration, but
37    // this implementation (based on a LHM) does. This will all be replaced soon anyways, so leaving
38    // it as is for now.
39    private static class ExpiringComputingMap<K, V> extends LinkedHashMap<K, V>
40        implements ConcurrentMap<K, V> {
41      private final long expirationMillis;
42      private final Function<? super K, ? extends V> computer;
43      private final int maximumSize;
44  
45      ExpiringComputingMap(
46          long expirationMillis, int maximumSize, int initialCapacity) {
47        this(expirationMillis, null, maximumSize, initialCapacity);
48      }
49  
50      ExpiringComputingMap(long expirationMillis, Function<? super K, ? extends V> computer,
51          int maximumSize, int initialCapacity) {
52        super(initialCapacity, /* ignored loadFactor */ 0.75f, (maximumSize != -1));
53        this.expirationMillis = expirationMillis;
54        this.computer = computer;
55        this.maximumSize = maximumSize;
56      }
57  
58      @Override
59      public V put(K key, V value) {
60        V result = super.put(key, value);
61        if (expirationMillis > 0) {
62          scheduleRemoval(key, value);
63        }
64        return result;
65      }
66  
67      @Override
68      protected boolean removeEldestEntry(Map.Entry<K, V> ignored) {
69        return (maximumSize == -1) ? false : size() > maximumSize;
70      }
71  
72      @Override
73      public V putIfAbsent(K key, V value) {
74        if (!containsKey(key)) {
75          return put(key, value);
76        } else {
77          return get(key);
78        }
79      }
80  
81      @Override
82      public boolean remove(Object key, Object value) {
83        if (containsKey(key) && get(key).equals(value)) {
84          remove(key);
85          return true;
86        }
87        return false;
88      }
89  
90      @Override
91      public boolean replace(K key, V oldValue, V newValue) {
92        if (containsKey(key) && get(key).equals(oldValue)) {
93          put(key, newValue);
94          return true;
95        }
96        return false;
97      }
98  
99      @Override
100     public V replace(K key, V value) {
101       return containsKey(key) ? put(key, value) : null;
102     }
103 
104     private void scheduleRemoval(final K key, final V value) {
105       // from MapMaker
106       /*
107        * TODO: Keep weak reference to map, too. Build a priority queue out of the entries themselves
108        * instead of creating a task per entry. Then, we could have one recurring task per map (which
109        * would clean the entire map and then reschedule itself depending upon when the next
110        * expiration comes). We also want to avoid removing an entry prematurely if the entry was set
111        * to the same value again.
112        */
113       Timer timer = new Timer() {
114         @Override
115         public void run() {
116           remove(key, value);
117         }
118       };
119       timer.schedule((int) expirationMillis);
120     }
121 
122     @Override
123     public V get(Object k) {
124       // from CustomConcurrentHashMap
125       V result = super.get(k);
126       if (result == null && computer != null) {
127         /*
128          * This cast isn't safe, but we can rely on the fact that K is almost always passed to
129          * Map.get(), and tools like IDEs and Findbugs can catch situations where this isn't the
130          * case.
131          *
132          * The alternative is to add an overloaded method, but the chances of a user calling get()
133          * instead of the new API and the risks inherent in adding a new API outweigh this little
134          * hole.
135          */
136         @SuppressWarnings("unchecked")
137         K key = (K) k;
138         result = compute(key);
139       }
140       return result;
141     }
142 
143     private V compute(K key) {
144       // from MapMaker
145       V value;
146       try {
147         value = computer.apply(key);
148       } catch (Throwable t) {
149         throw new ComputationException(t);
150       }
151 
152       if (value == null) {
153         String message = computer + " returned null for key " + key + ".";
154         throw new NullPointerException(message);
155       }
156       put(key, value);
157       return value;
158     }
159   }
160 
161   private int initialCapacity = 16;
162   private long expirationMillis = 0;
163   private int maximumSize = -1;
164   private boolean useCustomMap;
165 
166   public MapMaker() {}
167 
168   @Override
169   public MapMaker initialCapacity(int initialCapacity) {
170     if (initialCapacity < 0) {
171       throw new IllegalArgumentException();
172     }
173     this.initialCapacity = initialCapacity;
174     return this;
175   }
176 
177   @Override
178   MapMaker expireAfterWrite(long duration, TimeUnit unit) {
179     if (expirationMillis != 0) {
180       throw new IllegalStateException(
181           "expiration time of " + expirationMillis + " ns was already set");
182     }
183     if (duration <= 0) {
184       throw new IllegalArgumentException("invalid duration: " + duration);
185     }
186     this.expirationMillis = unit.toMillis(duration);
187     useCustomMap = true;
188     return this;
189   }
190 
191   @Override
192   MapMaker maximumSize(int maximumSize) {
193     if (this.maximumSize != -1) {
194       throw new IllegalStateException("maximum size of " + maximumSize + " was already set");
195     }
196     if (maximumSize < 0) {
197       throw new IllegalArgumentException("invalid maximum size: " + maximumSize);
198     }
199     this.maximumSize = maximumSize;
200     useCustomMap = true;
201     return this;
202   }
203 
204   @Override
205   public MapMaker concurrencyLevel(int concurrencyLevel) {
206     if (concurrencyLevel < 1) {
207       throw new IllegalArgumentException("GWT only supports a concurrency level of 1");
208     }
209     // GWT technically only supports concurrencyLevel == 1, but we silently
210     // ignore other positive values.
211     return this;
212   }
213 
214   @Override
215   public <K, V> ConcurrentMap<K, V> makeMap() {
216     return useCustomMap
217         ? new ExpiringComputingMap<K, V>(expirationMillis, null, maximumSize, initialCapacity)
218         : new ConcurrentHashMap<K, V>(initialCapacity);
219   }
220 
221   @Override
222   public <K, V> ConcurrentMap<K, V> makeComputingMap(Function<? super K, ? extends V> computer) {
223     return new ExpiringComputingMap<K, V>(
224         expirationMillis, computer, maximumSize, initialCapacity);
225   }
226 }